package models;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import play.libs.WS;
import play.libs.WS.WSRequest;

import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;

import siena.Column;
import siena.DateTime;
import siena.Generator;
import siena.Id;
import siena.Model;
import siena.NotNull;
import utils.AppConstants;
import utils.C2DM;

/**
 * This table contains the notification details. It is linked to the
 * NotificationReceiver table through a one-to-many relation to get its
 * receivers.
 * 
 * Please note that throughout this class the terms: "Owner" refers to the
 * person who creates the notification; "Sender" refers to the person who will
 * emit the notification; "Receiver" refers to the person who will be notified.
 * 
 * @author Dalia
 * 
 * 
 */
public class Notification extends Model {
	@Id(Generator.AUTO_INCREMENT)
	public long id; // notification id

	@Column("ntype")
	@NotNull
	int ntype; // type of notification:offer 0,ntype 1,alarm 2

	@Column("owner")
	@NotNull
	User owner; // who initiated this notification

	@Column("sender")
	@NotNull
	String phoneSender; // phone number; map to user if they are registered
	// send message otherwise

	@Column("place")
	@NotNull
	String place; // longitude and latitude coordinates

	@Column("start_date")
	Date startDate; // date to send notification

	@Column("end_date")
	Date expiryDate; // date to send notification

	@Column("type")
	@NotNull
	boolean type; // time or distance

	@Column("when")
	@NotNull
	int when; // the actual number; minutes or meters

	@Column("permission_status")
	String permission;

	@Column("sending_status")
	boolean sent;

	@Column("fulfilled")
	boolean fulfilled;

	@Column("timestamp")
	Date timestamp;

	/**
	 * Notification Constants
	 */
	static final String OWNER = "owner";
	static final String SENDER = "phoneSender";
	// static final String RECEIVER = "receiver";
	static final String PERMISSION = "permission";
	static final String ACCEPTED = AppConstants.PERMISSIONS[0];
	static final String SENT = "sent";
	static final String NTYPE = "ntype";

	static final String RECEIVER_REQUEST = "owner";
	static final String SENDER_REQUEST = "phoneSender";
	static final String START_DATE = "startDate";
	static final String END_DATE = "expiryDate";
	static final String HTTP_STATUS = "status";
	private static final String FulFilled = "fulfilled";
	private static final String MAPS_URL = "http://maps.googleapis.com/maps/api/distancematrix/json?origins=%s&destinations=%s&mode=driving&language=en-US&sensor=true";

	private static final String RESPONSE_OK = "OK";

	/**
	 * Default constructor. Sets default values if applicable.
	 */
	public Notification() {
		// TODO Auto-generated constructor stub
		super();
		this.sent = false;
		this.fulfilled = false;

		// are overridded in other constructors if needed
		// get current date and time
		Calendar cal = Calendar.getInstance();
		this.startDate = cal.getTime(); // assume date of creation
		this.timestamp = cal.getTime();
		// add 24 hours
		cal.add(Calendar.DATE, 1);
		this.expiryDate = cal.getTime(); // assume expires after 24 hours

		this.permission = ACCEPTED; // for ease of filtering
	}

	/**
	 * Most basic constructor, requires all the following:
	 * 
	 * @param ntype
	 * @param owner
	 * @param phoneSender
	 * @param place
	 * @param type
	 * @param when
	 */
	public Notification(int ntype, User owner, String phoneSender,
			String place, boolean type, int when) {
		this();
		this.ntype = ntype;
		this.owner = owner;
		this.phoneSender = phoneSender;
		this.place = place;
		this.type = type;
		this.when = when;

	}

	/**
	 * Offer with start/end dates
	 * 
	 * @param ntype
	 * @param owner
	 * @param phoneSender
	 * @param place
	 * @param startDate
	 * @param expiryDate
	 * @param type
	 * @param when
	 */
	public Notification(int ntype, User owner, String phoneSender,
			String place, Date startDate, Date expiryDate, boolean type,
			int when) {

		this(ntype, owner, phoneSender, place, type, when);
		if (startDate != null && expiryDate != null) {
			this.startDate = startDate;
			this.expiryDate = expiryDate;
		}

	}

	/**
	 * Request with permission
	 * 
	 * @param ntype
	 * @param owner
	 * @param phoneSender
	 * @param place
	 * @param type
	 * @param when
	 * @param permission
	 */
	public Notification(int ntype, User owner, String phoneSender,
			String place, boolean type, int when, String permission) {

		this(ntype, owner, phoneSender, place, type, when);
		this.permission = permission;
	}

	/**
	 * Request with permission and start/end dates
	 * 
	 * @param ntype
	 * @param owner
	 * @param phoneSender
	 * @param place
	 * @param startDate
	 * @param expiryDate
	 * @param type
	 * @param when
	 * @param permission
	 */
	public Notification(int ntype, User owner, String phoneSender,
			String place, Date startDate, Date expiryDate, boolean type,
			int when, String permission) {

		this(ntype, owner, phoneSender, place, type, when, permission);
		if (startDate != null && expiryDate != null) {
			this.startDate = startDate;
			this.expiryDate = expiryDate;
		}

	}

	/**
	 * Alarm without start/end dates
	 * 
	 * @param ntype
	 * @param owner
	 * @param phoneSender
	 * @param place
	 * @param type
	 * @param when
	 * @param timestamp
	 */
	public Notification(int ntype, User owner, String phoneSender,
			String place, boolean type, int when, Date timestamp) {

		this(ntype, owner, phoneSender, place, type, when);
		if(timestamp!=null)
			this.timestamp = timestamp;
	}

	/**
	 * Alarm with start/end dates
	 * 
	 * @param ntype
	 * @param owner
	 * @param phoneSender
	 * @param place
	 * @param startDate
	 * @param expiryDate
	 * @param type
	 * @param when
	 * @param timestamp
	 */
	public Notification(int ntype, User owner, String phoneSender,
			String place, Date startDate, Date expiryDate, boolean type,
			int when, Date timestamp) {

		this(ntype, owner, phoneSender, place, startDate, expiryDate, type,
				when);
		if(timestamp!=null)
			this.timestamp = timestamp;

	}

	public long getId() {
		return id;
	}

	public void setId(long id) {
		this.id = id;
	}

	public User getOwner() {
		return owner;
	}

	public void setOwner(User owner) {
		this.owner = owner;
	}

	public String getPhoneSender() {
		return phoneSender;
	}

	public void setPhoneSender(String phoneSender) {
		this.phoneSender = phoneSender;
	}

	public String getPlace() {
		return place;
	}

	public void setPlace(String place) {
		this.place = place;
	}

	public boolean isType() {
		return type;
	}

	public void setType(boolean type) {
		this.type = type;
	}

	public int getWhen() {
		return when;
	}

	public void setWhen(int when) {
		this.when = when;
	}

	public String getPermission() {
		return permission;
	}

	public void setPermission(String permission) {
		this.permission = permission;
	}

	public boolean isSent() {
		return sent;
	}

	public void setSent(boolean sent) {
		this.sent = sent;
	}

	public boolean isFulfilled() {
		return fulfilled;
	}

	public void setFulfilled(boolean fulfilled) {
		this.fulfilled = fulfilled;
	}

	public int getNtype() {
		return ntype;
	}

	public void setNtype(int ntype) {
		this.ntype = ntype;
	}

	public Date getStartDate() {
		return startDate;
	}

	public void setStartDate(Date startDate) {
		this.startDate = startDate;
	}

	public Date getExpiryDate() {
		return expiryDate;
	}

	public void setExpiryDate(Date expiryDate) {
		this.expiryDate = expiryDate;
	}

	public Date getTimestamp() {
		return timestamp;
	}

	public void setTimestamp(Date timestamp) {
		this.timestamp = timestamp;
	}

	/**
	 * get the phone number of the owner of the notification
	 * 
	 * @return
	 */
	public String getPhoneOwner() {
		return getOwner().fetchData().getPhone();
	}

	/**
	 * get list of receivers for any notification
	 * 
	 * @return List<Strings> phone numbers of receivers in case of request or
	 *         Alarm it's 1 receiver in the list. in case of offer list of
	 *         phones of receivers.
	 * @author Gad
	 */
	public List<String> getReceivers() {
		List<String> receiversPhones = new LinkedList<String>();
		if (this.getNtype() == AppConstants.REQUEST) {
			User receiver = this.getRequestReceiver();
			receiver = receiver.fetchData();
			receiversPhones.add(receiver.getPhone());
		} else if (this.getNtype() == AppConstants.OFFER) {
			receiversPhones.addAll(NotificationReceiver
					.getReceiversPhones(getId()));
		} else if (this.getNtype() == AppConstants.ALARM) {
			receiversPhones.add(getOwner().getPhone());
		}
		return receiversPhones;
	}

	@Override
	public String toString() {
		return "Notification [fulfilled=" + fulfilled + ", id=" + id
				+ ", owner=" + owner + ", permission=" + permission
				+ ", phoneSender=" + phoneSender + ", place=" + place
				+ ", sent=" + sent + ", type=" + type + ", when=" + when + "]";
	}

	public User getRequestReceiver() {
		return getOwner();

	}

	/**
	 * 
	 * Gets one notificatio identified by notif_id
	 * 
	 * @param notif_id
	 * @return
	 */
	public static Notification getNotification(long notif_id) {
		return all(Notification.class).filter("id", notif_id).get();
	}

	/**
	 * Retrieves all Offers set by a certain user
	 * 
	 * @author Dalia
	 * @param owner
	 * @return List of type Notification
	 */
	public static List<Notification> getOffersWhereOwnerIs(User owner) {
		// owner=sender!=receiver for Offers
		// String sender = owner.getPhone();
		/*
		 * List<Notification> result = new ArrayList<Notification>();
		 * List<Notification> ownerSender=
		 * (all(Notification.class).filter(OWNER, owner)) .filter("sender",
		 * sender).fetch(); //for each notification get receivers
		 * for(Notification n:ownerSender){ //only need to check the first
		 * receiver //if it's an alarm; first receiver will be
		 * self(owner,sender); ignore NotificationReceiver rec =
		 * NotificationReceiver.getReceiversFor(n.getId()).get(0); if
		 * (rec.getReceiver()!=sender)//if first receiver is not self(it's an
		 * Offer), add to result result.add(n); }
		 * 
		 * return result;
		 */
		return (all(Notification.class).filter(OWNER, owner)).filter(NTYPE,
				AppConstants.OFFER).fetch();
	}

	/**
	 * Retrieves all Requests set by a certain user
	 * 
	 * @author Dalia
	 * @param owner
	 * @return List of Type Notification
	 */
	public static List<Notification> getRequestsWhereOwnerIs(User owner) {
		// owner=receiver!=sender for Requests
		/*
		 * String ownerPhone = owner.getPhone(); //get where owner is receiver
		 * List<NotificationReceiver> rec = NotificationReceiver
		 * .getNotificationsWhereReceiverIs(ownerPhone); List<Notification>
		 * result = new ArrayList<Notification>(); //check if owner is different
		 * from sender for (NotificationReceiver nr:rec){ Notification n =
		 * getNotification(nr.getNotif_id()); if(n.getPhoneSender()!=ownerPhone)
		 * result.add(n); }
		 * 
		 * return result;
		 */
		return (all(Notification.class).filter(OWNER, owner)).filter(NTYPE,
				AppConstants.REQUEST).fetch();

	}
	
	

	/**
	 * Retrieves all alarms set by a certain user
	 * 
	 * @author Dalia
	 * @param owner
	 * @return List of Type Notification
	 * 
	 */
	public static List<Notification> getAlamrsWhereOwnerIs(User owner) {
		// owner=sender=receiver for Alarms
		/*
		 * String sender_receiver = owner.getPhone(); return
		 * all(Notification.class).filter(OWNER, owner) .filter("sender",
		 * sender_receiver) .filter("receiver", sender_receiver) .fetch();
		 */
		return (all(Notification.class).filter(OWNER, owner)).filter(NTYPE,
				AppConstants.ALARM).fetch();

	}

	/**
	 * Retrieves all requests for a certain sender.
	 * 
	 * @param sender
	 * @return
	 */
	public static List<Notification> getRequestsWhereSenderIs(User sender) {
		return all(Notification.class).filter(NTYPE, AppConstants.REQUEST)
				.filter(SENDER, sender.getPhone()).fetch();
	}

	/**
	 * @param phone
	 *            : The phone number of the receiver
	 * @author Gad,Dalia
	 * @return All accepted requests where client with phone is receiver
	 */
	public static List<Notification> getAcceptedRequestsWhereReceiverIs(
			String phone) {
		User owner = User.findByPhone(phone);
		return (all(Notification.class).filter(OWNER, owner))
				.filter(NTYPE, AppConstants.REQUEST)
				.filter(PERMISSION, ACCEPTED).fetch();
	}

	/**
	 * Retrieves all requests that need to be sent. The input is the receiver's
	 * phone EDITED BY GAD the unsentRequests are requested with no permission
	 * so the SENDER_REQUEST should get to accept or reject
	 * 
	 * @param phone
	 * @return
	 * 
	 */
	public static List<Notification> getAllUnsentRequestsTo(String phone) {
		// User requestedFrom = User.findByPhone(phone);
		// as sender here is just a phone number
		// the sender should accept not the owner
		return (((all(Notification.class).filter(SENDER_REQUEST, phone))
				.filter(SENT, false)).filter(NTYPE, AppConstants.REQUEST)).fetch();
	}

	/**
	 * Gets all the accepted notifications of some phone number which their
	 * start date is passed and expire date after current date (if they are set)
	 * 
	 * @param phone
	 *            the phone number of the notification sender
	 * @return list of notifications
	 * 
	 * @author Gad
	 * @param now 
	 */
	public static List<Notification> getActiveNotFulfilledNotificationsForSender(
			String phone, Date currentTime) {
		// User sender = User.findByPhone(phone);
		
		System.err.println(currentTime.toString());
		List<Notification> partiallyAccepted = (all(Notification.class)
				.filter(SENDER, phone).filter(PERMISSION, ACCEPTED).filter(
				FulFilled, false)).fetch();
		List<Notification> acceptedNotifications= new LinkedList<Notification>();
		int size = partiallyAccepted.size();
		System.err.println("from db "+size);
		// exclude out of time interval notifications
		for (int i = 0; i < size; i++) {
			Notification cu = partiallyAccepted.get(i);
			Date sDate = cu.getStartDate();
			Date eDate = cu.getExpiryDate();
			if (sDate != null && currentTime.before(sDate)) {
				System.out.println("start"+sDate.toString());
				continue;
			} else if (eDate != null && currentTime.after(eDate)) {
				System.out.println("end " +eDate.toString());
				continue;

			} else if (cu.getPlace() == null || cu.getPlace().equals("")) {
				System.out.println(cu.getPlace());
				continue;
			}
			acceptedNotifications.add(cu);
		}
		return acceptedNotifications;
	}

	/**
	 * This method checks for of receiver with phone number and query google
	 * maps to find which are fulfilled to notification conditions
	 * 
	 * @param phone
	 * @param longitude
	 * @param latitude
	 * @author Gad
	 * @param now 
	 */
	public static void markFulfilledRequests(String phone, double longitude,
			double latitude, Date now) {
		// User sender = User.findByPhone(phone);
		// get notifications to process
		List<Notification> notifications = Notification
				.getActiveNotFulfilledNotificationsForSender(phone,now);
		System.out.println("in fulfill");
		if (notifications.size() == 0) {
			return;
		}
		// connect to google maps
		WSRequest connecrtToMaps=WS.url(MAPS_URL, (latitude + "," + longitude),locationsListToString(notifications));
		System.err.println(connecrtToMaps.url);
		JsonObject timeDist =connecrtToMaps.get().getJson()
				.getAsJsonObject();
	
		System.err.println(timeDist.toString());
		if (timeDist != null
				&& timeDist.get(HTTP_STATUS).getAsString().equals(RESPONSE_OK)) {
			long[] response = parseJsonResponse(timeDist.getAsJsonObject());
			if (response.length != 0 && notifications.size() != 0)
				updateFulFilledFlag(notifications, response);
		}

		notifyFulfillingToReceivers(notifications);

	}

	/**
	 * Sends notifications to all users that their notifications has been
	 * fulfilled
	 * 
	 * @param notifications
	 *            list of notification that its receiver/s should be notified
	 * @author Gad
	 */
	private static void notifyFulfillingToReceivers(
			final List<Notification> notifications) {
		// in a thread in order to don't hang the client
		// new Thread(new Runnable() {
		//
		// @Override
		// public void run() {
		for (Notification notif : notifications) {
			if (!notif.isFulfilled())
				continue;
			List<String> receiversPhones = notif.getReceivers();
			C2DM.pushForgroup(receiversPhones, AppConstants.NEW_ARRIVAL_CK,
					AppConstants.NEW_ARRIVAL_MSG);
		}
		// }
		// }).start();
	}

	/**
	 * Setting the fulfilled flag according to result array returned
	 * 
	 * @param offers
	 *            list of requests to be evaluated
	 * @param res
	 *            this array should be 2*offers.size() as for each record in
	 *            offers 2 cells in res 1st the duration and 2nd the distance
	 *            from the target
	 * @author gad
	 */
	private static void updateFulFilledFlag(List<Notification> notifs,
			long[] res) {
		int i = 0;
		for (Notification n : notifs) {
			if (!n.isType()) // by time
			{
				if ((n.getWhen() * 60) >= res[i]) {
					n.setFulfilled(true);
					n.update();
				}
			} else if (n.isType()) {// by range
				if (n.getWhen() >= res[i + 1]) {
					n.setFulfilled(true);
					n.update();
				}
			}
			i += 2;
		}
	}

	/**
	 * parsing Json response to get distance and duration
	 * 
	 * @param responseObj
	 * @return long[]
	 * @author Gad
	 */
	private static long[] parseJsonResponse(JsonObject responseObj) {
		if (!responseObj.get(HTTP_STATUS).getAsString().equals(RESPONSE_OK))
			return new long[0];
		JsonArray row = responseObj.get("rows").getAsJsonArray();
		System.out.println(row.toString() + '\n');
		JsonArray jsonResponses = row.get(0).getAsJsonObject().get("elements")
				.getAsJsonArray();
		System.out.println(jsonResponses.toString() + '\n');
		long[] response = new long[2 * jsonResponses.size()];
		int i = 0;
		for (JsonElement jsonElement : jsonResponses) {

			JsonObject res = jsonElement.getAsJsonObject();
			if (res.get(HTTP_STATUS).getAsString().equals(RESPONSE_OK)) {
				long distance = res.get("distance").getAsJsonObject()
						.get("value").getAsLong();
				long duration = res.get("duration").getAsJsonObject()
						.get("value").getAsLong();
				response[i++] = duration;
				response[i++] = distance;
			} else {
				response[i++] = Long.MAX_VALUE;
				response[i++] = Long.MAX_VALUE;
			}

			// System.out.println(response[i - 2] + " " + response[i - 1]);
		}
		return response;
	}

	/**
	 * Gets the targets from the offers and concatenate them separated with | to
	 * be used with google maps query
	 * 
	 * @param offers
	 * @return String
	 * @author Gad
	 */
	private static String locationsListToString(List<Notification> notifs) {
		String out = "";

		for (Notification n : notifs) {
			out += ("|" + n.getPlace());
			System.out.println(n.getPlace());
		}
		out = out.substring(1);
		return out;
	}

}
